/*
 * Copyright 2015-2016 Tumblr, Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.tumblr.backboard;

import com.facebook.rebound.Spring;
import com.facebook.rebound.SpringListener;
import com.facebook.rebound.SpringSystem;
import com.tumblr.backboard.Utils.MathUtils;
import com.tumblr.backboard.Utils.Property;
import com.tumblr.backboard.imitator.EventImitator;
import com.tumblr.backboard.imitator.Imitator;
import com.tumblr.backboard.imitator.MotionImitator;
import com.tumblr.backboard.performer.Performer;
import ohos.agp.components.Component;
import ohos.multimodalinput.event.TouchEvent;

import java.util.ArrayList;
import java.util.List;

/**
 * Coordinates the relationship betweenMotionImitator,Spring, and Performer on single Component. This primarily
 * exists to manage the OnTouchListener on the
 *
 * @author ericleong
 * @since 2021-05-07
 */
public final class Actor {
    /**
     * Distance in pixels that can be moved before touch is no longer considered an "click".
     */
    public static final int MAX_CLICK_DISTANCE = 10;

    private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500;

    private static final int VALUE_TWO = 2;

    /**
     * Contains the imitators and listeners coupled to an single spring.
     *
     * @author ericleong
     * @since 2021-05-07
     */
    public static final class Motion {
        private Spring spring;

        private EventImitator[] imitators;

        private Performer[] performers;
        private SpringListener[] springListeners;

        private Motion(Spring spring, EventImitator imitator, Performer[] performers,
                SpringListener[] springListeners) {
            this(spring, new EventImitator[]{imitator}, performers, springListeners);
        }

        private Motion(Spring spring, Performer[] performers, SpringListener[] springListeners) {
            this.imitators = new MotionImitator[0];
            this.performers = performers;
            this.spring = spring;
            this.springListeners = springListeners;
        }

        private Motion(Spring spring, EventImitator[] imitators, final Performer[] performers,
                SpringListener[] springListeners) {
            this.imitators = imitators;
            this.performers = performers;
            this.spring = spring;
            this.springListeners = springListeners;
        }

        /**
         * getSpring
         *
         * @return spring
         */
        public Spring getSpring() {
            return spring;
        }

        /**
         * getImitators
         *
         * @return imitators.clone
         */
        public EventImitator[] getImitators() {
            return imitators.clone();
        }
    }

    private Component mView;

    private List<Motion> mMotions;

    private MotionListener mMotionListener;
    private Component.TouchEventListener mOnTouchListener;
    /**
     * Allows the user to disable the motion listener.
     */
    private boolean mIsMotionListenerEnabled;
    /**
     * Prevent parent from intercepting touch events (useful when in lists).
     */
    private boolean mIsRequestDisallowTouchEvent;

    private Actor(Component view, List<Motion> motions, Component.TouchEventListener onTouchListener,
            boolean isMotionListenerEnabled, boolean isAttachTouchListener, boolean isRequestDisallowTouchEvent) {
        mView = view;
        mMotions = motions;
        mOnTouchListener = onTouchListener;
        mMotionListener = new MotionListener();
        mIsMotionListenerEnabled = isMotionListenerEnabled;
        mIsRequestDisallowTouchEvent = isRequestDisallowTouchEvent;
        if (isAttachTouchListener) {
            view.setTouchEventListener(mMotionListener);
        }
    }

    /**
     * getOnTouchListener
     *
     * @return mOnTouchListener
     */
    public Component.TouchEventListener getOnTouchListener() {
        return mOnTouchListener;
    }

    /**
     * getMotionListener
     *
     * @return mMotionListener
     */
    public Component.TouchEventListener getMotionListener() {
        return mMotionListener;
    }

    /**
     * getView
     *
     * @return mView
     */
    public Component getView() {
        return mView;
    }

    /**
     * getMotions
     *
     * @return mMotions
     */
    public List<Motion> getMotions() {
        return mMotions;
    }

    /**
     * isTouchEnabled
     *
     * @return mIsMotionListenerEnabled
     */
    public boolean isTouchEnabled() {
        return mIsMotionListenerEnabled;
    }

    /**
     * setTouchEnabled
     *
     * @param isEnabled isEnabled
     */
    public void setTouchEnabled(boolean isEnabled) {
        this.mIsMotionListenerEnabled = isEnabled;
    }

    /**
     * Removes all spring listeners controlled by this {Actor}.
     */
    public void removeAllListeners() {
        for (Motion motion : mMotions) {
            for (Performer performer : motion.performers) {
                motion.spring.removeListener(performer);
            }

            if (motion.springListeners != null) {
                for (SpringListener listener : motion.springListeners) {
                    motion.spring.removeListener(listener);
                }
            }
        }
    }

    /**
     * Adds all spring listeners back.
     */
    public void addAllListeners() {
        for (Motion motion : mMotions) {
            for (Performer performer : motion.performers) {
                motion.spring.addListener(performer);
            }

            if (motion.springListeners != null) {
                for (SpringListener listener : motion.springListeners) {
                    motion.spring.addListener(listener);
                }
            }
        }
    }

    /**
     * Implements the builder pattern for {Actor}.
     *
     * @author ericleong
     * @since 2021-05-07
     */
    public static class Builder {
        private Component mView;
        private List<Motion> mMotions = new ArrayList<Motion>();
        private Component.TouchEventListener mOnTouchListener;
        private SpringSystem mSpringSystem;
        private boolean mIsMotionListenerEnabled = true;
        private boolean mIsAttachMotionListener = true;
        private boolean mIsRequestDisallowTouchEvent;
        private boolean mIsAttachSpringListeners = true;

        /**
         * Animates the given view with the default {com.facebook.rebound.SpringConfig} and automatically creates
         * an {com.facebook.rebound.SpringSystem}.
         *
         * @param springSystem the spring system to use
         * @param view the view to animate
         */
        public Builder(SpringSystem springSystem, Component view) {
            mView = view;
            mSpringSystem = springSystem;
        }

        /**
         * on touch listener
         *
         * @param onTouchListener an touch listener to pass touch events to
         * @return this builder for chaining
         */
        public Builder onTouchListener(Component.TouchEventListener onTouchListener) {
            mOnTouchListener = onTouchListener;
            return this;
        }

        /**
         * Uses the default {com.facebook.rebound.SpringConfig} to animate the view.
         *
         * @param properties the event fields to imitate and the view properties to animate.
         * @return this builder for chaining
         */
        public Builder addTranslateMotion(MotionProperty... properties) {
            return addMotion(mSpringSystem.createSpring(), properties);
        }

        /**
         * Uses the default {com.facebook.rebound.SpringConfig} to animate the view.
         *
         * @param property the event field to imitate and the view property to animate.
         * @param listener an listener to call
         * @return this builder for chaining
         */
        public Builder addTranslateMotion(MotionProperty property, SpringListener listener) {
            return addMotion(mSpringSystem.createSpring(), Imitator.TRACK_ABSOLUTE, Imitator.FOLLOW_EXACT,
                    new MotionProperty[]{property}, new SpringListener[]{listener});
        }

        /**
         * Uses the default {com.facebook.rebound.SpringConfig} to animate the view.
         *
         * @param trackStrategy the tracking behavior
         * @param followStrategy the follow behavior
         * @param properties the event fields to imitate and the view properties to animate.
         * @return this builder for chaining
         */
        public Builder addTranslateMotion(int trackStrategy, int followStrategy, MotionProperty... properties) {
            return addMotion(mSpringSystem.createSpring(), trackStrategy, followStrategy, properties);
        }

        /**
         * Uses the default {com.facebook.rebound.SpringConfig} to animate the view.
         *
         * @param trackStrategy the tracking behavior
         * @param followStrategy the follow behavior
         * @param restValue the rest value of the spring
         * @param properties the event fields to imitate and the view properties to animate.
         * @return this builder for chaining
         */
        public Builder addTranslateMotion(int trackStrategy, int followStrategy, int restValue,
                MotionProperty... properties) {
            return addMotion(mSpringSystem.createSpring(), trackStrategy, followStrategy, restValue, properties);
        }

        /**
         * add motion
         *
         * @param spring the underlying {com.facebook.rebound.Spring}.
         * @param properties the event fields to imitate and the view properties to animate.
         * @return this builder for chaining
         */
        public Builder addMotion(Spring spring, MotionProperty... properties) {
            return addMotion(spring, Imitator.TRACK_ABSOLUTE, Imitator.FOLLOW_EXACT, properties);
        }

        /**
         * add motion
         *
         * @param spring the underlying {com.facebook.rebound.Spring}.
         * @param trackStrategy the tracking behavior
         * @param followStrategy the follow behavior
         * @param properties the event fields to imitate and the view properties to animate.
         * @return this builder for chaining
         */

        public Builder addMotion(Spring spring, int trackStrategy, int followStrategy, MotionProperty... properties) {
            mMotions.add(createMotionFromProperties(spring, properties, null, trackStrategy, followStrategy, 0));
            return this;
        }

        /**
         * add motion
         *
         * @param spring the underlying {com.facebook.rebound.Spring}.
         * @param trackStrategy the tracking behavior
         * @param followStrategy the follow behavior
         * @param restValue the rest value
         * @param properties the event fields to imitate and the view properties to animate.
         * @return this builder for chaining
         */
        public Builder addMotion(Spring spring, int trackStrategy, int followStrategy, int restValue,
                MotionProperty... properties) {
            mMotions.add(createMotionFromProperties(spring, properties, null, trackStrategy, followStrategy,
                    restValue));
            return this;
        }

        /**
         * add motion
         *
         * @param spring the underlying {com.facebook.rebound.Spring}.
         * @param trackStrategy the tracking behavior
         * @param followStrategy the follow behavior
         * @param restValue the rest value
         * @param property the event fields to imitate and the view property to animate.
         * @param springListener an spring listener to attach to the spring
         * @return this builder for chaining
         */
        public Builder addMotion(Spring spring, int trackStrategy, int followStrategy, int restValue,
                MotionProperty property, SpringListener springListener) {
            mMotions.add(createMotionFromProperties(spring, new MotionProperty[]{property},
                    new SpringListener[]{springListener}, trackStrategy, followStrategy, restValue));
            return this;
        }

        /**
         * add motion
         *
         * @param spring the underlying {com.facebook.rebound.Spring}.
         * @param trackStrategy the tracking behavior
         * @param followStrategy the follow behavior
         * @param properties the event fields to imitate and the view properties to animate.
         * @param springListeners an array of spring listeners to attach to the spring
         * @return this builder for chaining
         */
        public Builder addMotion(Spring spring, int trackStrategy, int followStrategy, MotionProperty[] properties,
                SpringListener[] springListeners) {
            mMotions.add(createMotionFromProperties(spring, properties, springListeners, trackStrategy,
                    followStrategy, 0));
            return this;
        }

        /**
         * Uses an default {com.facebook.rebound.SpringConfig}.
         *
         * @param eventImitator maps an event to an {com.facebook.rebound.Spring}
         * @param viewProperties the Component property to animate
         * @return the builder for chaining
         */
        public Builder addMotion(EventImitator eventImitator, Property<Component, Float>... viewProperties) {
            Performer[] performers = new Performer[viewProperties.length];
            for (int index = 0; index < viewProperties.length; index++) {
                performers[index] = new Performer(viewProperties[index]);
            }
            return addMotion(mSpringSystem.createSpring(), eventImitator, performers);
        }

        /**
         * Uses an default {com.facebook.rebound.SpringConfig}.
         *
         * @param eventImitator maps an event to an {com.facebook.rebound.Spring}
         * @param performers map the {com.facebook.rebound.Spring} to an Component
         * @return the builder for chaining
         */
        public Builder addMotion(EventImitator eventImitator, Performer... performers) {
            return addMotion(mSpringSystem.createSpring(), eventImitator, performers);
        }

        /**
         * addMotion
         *
         * @param spring the underlying {com.facebook.rebound.Spring}.
         * @param eventImitator maps an event to an {com.facebook.rebound.Spring}
         * @param performers map the {com.facebook.rebound.Spring} to an Component
         * @return the builder for chaining
         */
        public Builder addMotion(Spring spring, EventImitator eventImitator, Performer... performers) {
            Motion motion = new Motion(spring, eventImitator, performers, null);

            // connect actors
            motion.imitators[0].setSpring(motion.spring);
            for (Performer performer : motion.performers) {
                performer.setTarget(mView);
            }
            mMotions.add(motion);
            return this;
        }

        /**
         * addMotion
         *
         * @param spring the underlying {com.facebook.rebound.Spring}.
         * @param eventImitator maps an event to an {com.facebook.rebound.Spring}
         * @param performers map the {com.facebook.rebound.Spring} to an Component
         * @param springListeners additional listeners to attach
         * @return the builder for chaining
         */
        public Builder addMotion(Spring spring, EventImitator eventImitator, Performer[] performers,
                SpringListener[] springListeners) {
            // create struct
            Motion motion = new Motion(spring, eventImitator, performers, springListeners);

            // connect actors
            motion.imitators[0].setSpring(motion.spring);
            for (Performer performer : motion.performers) {
                performer.setTarget(mView);
            }
            mMotions.add(motion);
            return this;
        }

        /**
         * add motion
         *
         * @param motionImitator maps an event to an {com.facebook.rebound.Spring}
         * @param viewProperty the component property to animate
         * @param springListener additional listener to attach
         * @return the builder for chaining
         */
        public Builder addMotion(MotionImitator motionImitator, Property<Component, Float> viewProperty,
                SpringListener springListener) {
            return addMotion(mSpringSystem.createSpring(), motionImitator,
                    new Performer[]{new Performer(viewProperty)}, new SpringListener[]{springListener});
        }

        /**
         * request disallow touchevent
         *
         * @return flag to tell the attached OnTouchListener to call
         */
        public Builder requestDisallowTouchEvent() {
            mIsRequestDisallowTouchEvent = true;
            return this;
        }

        /**
         * A flag to tell this {Actor} not to attach the touch listener to the view.
         *
         * @return the builder for chaining
         */
        public Builder dontAttachMotionListener() {
            mIsAttachMotionListener = false;
            return this;
        }

        /**
         * A flag to tell this builder not to attach the spring listeners to the spring. They can be added with {@link
         * Actor#addAllListeners()}.
         *
         * @return the builder for chaining
         */
        public Builder dontAttachSpringListeners() {
            mIsAttachSpringListeners = false;
            return this;
        }

        /**
         * Creations an new motion object.
         *
         * @param spring the spring to use
         * @param motionProperties the properties of the event to track
         * @param springListeners additional spring listeners to add
         * @param trackStrategy the tracking strategy
         * @param followStrategy the follow strategy
         * @param restValue the spring rest value
         * @return an motion object
         */
        private Motion createMotionFromProperties(Spring spring, MotionProperty[] motionProperties,
                SpringListener[] springListeners, int trackStrategy, int followStrategy, int restValue) {
            MotionImitator[] motionImitators = new MotionImitator[motionProperties.length];
            Performer[] performers = new Performer[motionProperties.length];

            for (int index = 0; index < motionProperties.length; index++) {
                MotionProperty property = motionProperties[index];
                motionImitators[index] = new MotionImitator(spring, property, restValue, trackStrategy, followStrategy);
                performers[index] = new Performer(mView, property.getViewProperty());
            }
            return new Motion(spring, motionImitators, performers, springListeners);
        }

        /**
         * build
         *
         * @return Builds the {Actor}.
         */
        public Actor build() {
            // make connections
            Actor actor = new Actor(mView, mMotions, mOnTouchListener, mIsMotionListenerEnabled,
                    mIsAttachMotionListener, mIsRequestDisallowTouchEvent);
            if (mIsAttachSpringListeners) {
                actor.addAllListeners();
            }
            return actor;
        }
    }

    /**
     * motion listener
     *
     * @author ericleong
     * @since 2021-05-07
     */
    private class MotionListener implements Component.TouchEventListener {
        @Override
        public boolean onTouchEvent(Component v, TouchEvent event) {
            boolean isRetVal;
            if (!mIsMotionListenerEnabled || mMotions.isEmpty()) {
                if (mOnTouchListener != null) {
                    isRetVal = mOnTouchListener.onTouchEvent(v, event);
                } else {
                    isRetVal = false;
                }
                return isRetVal;
            }

            for (Motion motion : mMotions) {
                for (EventImitator imitator : motion.imitators) {
                    imitator.imitate(v, event);
                }
            }

            if (mOnTouchListener != null) {
                isRetVal = mOnTouchListener.onTouchEvent(v, event);
            } else {
                isRetVal = true;
            }

            if (mIsRequestDisallowTouchEvent) {
                // prevents parent from scrolling or otherwise stealing touch events
                v.setTouchEventListener(null);
            }

            if (v.isClickable()) {
                if (event.getOccurredTime() - event.getStartTime() > DEFAULT_LONG_PRESS_TIMEOUT) {
                    v.setClickable(false);
                    return true;
                }

                if (event.getPointerCount() > 0) {
                    float deltaX = MathUtils.floatToSubtract(event.getPointerPosition(
                            event.getPointerCount() - 1).getX(), event.getPointerPosition(0).getX());
                    float deltaY = MathUtils.floatToSubtract(event.getPointerPosition(
                            event.getPointerCount() - 1).getY(), event.getPointerPosition(0).getY());

                    // if user has moved too far, it is no longer an click
                    boolean isRemoveClickState = Math.pow(deltaX, VALUE_TWO) + Math.pow(deltaY, VALUE_TWO)
                            > Math.pow(MAX_CLICK_DISTANCE, VALUE_TWO);
                    v.setClickable(!isRemoveClickState);
                    return isRemoveClickState;
                } else {
                    return false;
                }
            }
            return isRetVal;
        }
    }
}
